package com.hanter.downloader.service;

import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.alibaba.fastjson.JSON;
import com.hanter.downloader.Delivery;
import com.hanter.downloader.Downloader;
import com.hanter.downloader.Partial;
import com.hanter.downloader.Response;
import com.hanter.downloader.TaskInfo;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.math.BigInteger;
import java.nio.channels.ClosedByInterruptException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;


public class DownloadTask implements Runnable, OnProgressIncreaseListener {

    public static final String TAG = DownloadTask.class.getSimpleName();

    @NonNull
    private final Context context;
    @NonNull
    private final Downloader.Request request;
    @NonNull
    private final ThreadPoolExecutor executor;
    @NonNull
    private final Delivery delivery;
    @NonNull
    private final DownloadService downloadService;
    @Nullable
    private TaskInfo taskInfo;
    @Nullable
    private List<PartialTask> partialTasks;
    @Nullable
    private List<AtomicLong> partialProgresses;
    @NonNull
    private AtomicLong progress;

    private volatile long mills;

    public DownloadTask(@NonNull Context context, @NonNull ThreadPoolExecutor executor, @NonNull Downloader.Request request, @NonNull Delivery delivery) {
        this.context = context.getApplicationContext();
        this.executor = executor;
        this.request = request;
        this.downloadService = new HttpUrlConnectionService(Downloader.get(context).getConfig());
        this.delivery = delivery;
        this.progress = new AtomicLong();
    }

    @Override
    public void onIncrease(long delta) {
        updateProgress(this.progress.addAndGet(delta));
    }

    private void updateProgress(int index, long delta) {
        this.onIncrease(delta);

        if (this.partialProgresses == null)
            return;

        AtomicLong progress = this.partialProgresses.get(index);
        progress.addAndGet(delta);

        // 保存进度
        if (this.taskInfo != null && this.taskInfo.getPartials() != null && this.partialTasks != null
                && SystemClock.elapsedRealtime() - mills > 400) {
            for (int i = 0; i < this.taskInfo.getPartials().size(); i++) {
                // offset
                Partial partial = this.taskInfo.getPartials().get(i);
                partial.setOffset(this.partialProgresses.get(i).get());
            }
            saveTask();
        }
    }

    private void updateProgress(long val) {
        Log.d(TAG, "progress: " + val);
        if (this.taskInfo != null) {
            this.delivery.postProgress(this.request, val, this.taskInfo.getContentLength());
        }
    }

    @Override
    public void run() {
        this.request.setThread(Thread.currentThread());

        // 先从本地获取是否已有任务，否则从网络获取长度
        taskInfo = getLocalTask();

        // 已经下载成功，检查文件是否存在，否则从新开始任务
        /*
        if (taskInfo != null && taskInfo.getStatus() == TaskInfo.STATUS_SUCCESSFUL) {
            // FIXME 解决文件判断， 判断文件是否存在 大小是否一样 甚至 摘要是否一样
            String filePath = this.taskInfo.getPath();
            this.delivery.postResponse(this.request);
            return;
        }
        */

        if (taskInfo == null)
            taskInfo = downloadService.getTaskInfo(request.getUrl());


        if (this.request.isCanceled()) {
            this.delivery.postCancel(this.request);
            return;
        }

        if (taskInfo == null) {
            this.request.setThread(null);
            this.delivery.postError(this.request, 205, "打开链接失败");
            return;
        }

        mills = SystemClock.elapsedRealtime();

        // 可能允许FilePath为null，保存默认目录
        String filePath = this.request.getFilePath();
        taskInfo.setName(this.request.getUrl());
        taskInfo.setPath(filePath);
        if (taskInfo.getPartials() != null && taskInfo.getPartials().size() > 0) {
            // 准备分块
            partialTasks = new ArrayList<>();
            partialProgresses = new ArrayList<>();
            CountDownLatch latch = new CountDownLatch(taskInfo.getPartials().size());

            for (int i = 0; i < taskInfo.getPartials().size(); i++) {
                Partial partial = taskInfo.getPartials().get(i);
                updateProgress(this.progress.addAndGet(partial.getOffset()));
                PartialTask task = new PartialTask(this, latch, i, partial);
                partialTasks.add(task);
                partialProgresses.add(new AtomicLong(partial.getOffset()));
                this.executor.execute(task);
            }

            try {
                latch.await();
                boolean success = true;
                for (PartialTask task : partialTasks) {
                    if (!task.isCompleted()) {
                        success = false;
                    }
                }
                // 下载成功
                if (success) {
                    this.delivery.postComplete(this.request);
                } else {
                    Log.d(TAG, "下载失败");
                    this.delivery.postError(this.request, Response.ERROR_CANCELED, "下载失败");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                if (partialTasks != null) {
                    for (PartialTask task : partialTasks) {
                        // 移除并取消，中断正在执行的
                        this.executor.remove(task);
                        task.cancel();
                    }
                }

                Log.d(TAG, "下载失败，取消");
                if (this.request.isCanceled()) {
                    this.delivery.postCancel(this.request);
                } else {
                    this.delivery.postError(this.request, Response.ERROR_CANCELED, "下载失败");
                }
            }
        } else {
            try {
                long len;
                if (filePath.startsWith(ContentResolver.SCHEME_CONTENT) || filePath.startsWith(ContentResolver.SCHEME_FILE)) {
                    // Uri资源
                    len = downloadService.download(context, Uri.parse(filePath), this.request.getUrl(), -1, -1, this);
                } else {
                    // 文件资源
                    len = downloadService.download(new File(filePath), this.request.getUrl(), this);
                }
                if (len == taskInfo.getContentLength()) {
                    Log.d(TAG, "下载成功");
                    this.delivery.postComplete(this.request);
                } else {
                    Log.d(TAG, "下载失败");
                    this.delivery.postError(this.request, Response.ERROR_FAILED, "下载中断");
                }
            } catch (ClosedByInterruptException | InterruptedIOException | InterruptedException e) {
                this.delivery.postError(this.request, Response.ERROR_CANCELED, "下载中断");
            } catch (IOException e) {
                e.printStackTrace();
                this.delivery.postError(this.request, Response.ERROR_FAILED, "下载中断");
            }
        }
        taskInfo.setStatus(TaskInfo.STATUS_SUCCESSFUL);
        saveTask();
        this.request.setThread(null);
    }

    @Nullable
    private TaskInfo getLocalTask() {
        File dir = new File(context.getFilesDir(), "downloader_files");
        String key = getMd5(this.request.getUrl());
        if (key == null) {
            Log.w(TAG, "task key generate failed.");
            return null;
        }

        File taskFile = new File(dir, key);

        if (!taskFile.exists()) return null;

        try {
            FileInputStream fis = new FileInputStream(taskFile);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            int len;
            byte[] buff = new byte[4096];
            while ((len = fis.read(buff)) != -1) {
                bos.write(buff, 0, len);
            }

            String json = new String(bos.toByteArray(), StandardCharsets.UTF_8);
            return JSON.parseObject(json, TaskInfo.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    private void saveTask() {
        Log.d(TAG, "save task.");

        if (this.taskInfo == null) return;

        File dir = new File(context.getFilesDir(), "downloader_files");
        String key = getMd5(this.request.getUrl());
        if (key == null) {
            Log.w(TAG, "task key generate failed.");
            return;
        }

        File taskFile = new File(dir, key);
        // 创建目录 和 文件
        try {
            if ((dir.exists() || dir.mkdirs()) && (taskFile.exists() || taskFile.createNewFile())) {
                String json = JSON.toJSONString(this.taskInfo);
                try (FileOutputStream fos = new FileOutputStream(taskFile)) {
                    fos.write(json.getBytes());
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                    taskFile.delete();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static String getMd5(String str){
        MessageDigest md;
        try {
            md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes(), 0, str.length());
            StringBuilder md5 = new StringBuilder(new BigInteger(1, md.digest()).toString(16));
            while ( md5.length() < 32 ) {
                md5.insert(0, "0");
            }
            return md5.toString();
        } catch (NoSuchAlgorithmException e) {
            System.out.println("Exception while encrypting to md5");
            e.printStackTrace();
        }
        return null;
    }

    private static class PartialTask implements Runnable {

        private final int index;
        private final DownloadTask task;
        private final CountDownLatch latch;
        private final long initOffset;
        private long len;
        private final Partial partial;
        private volatile Thread runner;

        PartialTask(DownloadTask task, CountDownLatch latch, int index, Partial partial) {
            this.task = task;
            this.latch = latch;
            this.index = index;
            this.partial = partial;
            this.initOffset = partial.getOffset();
            this.len = 0;
        }

        @Override
        public void run() {
            this.runner = Thread.currentThread();
            try {
                String filePath = this.task.request.getFilePath();
                // TODO 同步更新进度，并发问题？
                if (filePath.startsWith(ContentResolver.SCHEME_CONTENT) || filePath.startsWith(ContentResolver.SCHEME_FILE)) {
                    // Uri资源
                    len = task.downloadService.download(task.context, Uri.parse(filePath), task.request.getUrl(),
                            partial.getStart() + partial.getOffset(), partial.getEnd(), new OnProgressIncreaseListener() {
                                @Override
                                public void onIncrease(long delta) {
                                    task.updateProgress(index, delta);
                                }
                            });
                } else {
                    // 文件资源
                    len = task.downloadService.download(new File(filePath), task.request.getUrl(),
                            partial.getStart() + partial.getOffset(), partial.getEnd(), new OnProgressIncreaseListener() {
                                @Override
                                public void onIncrease(long delta) {
                                    task.updateProgress(index, delta);
                                }
                            });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                this.latch.countDown();
                this.runner = null;
            }
        }

        public void cancel() {
            if (this.runner != null) {
                synchronized (PartialTask.class) {
                    if (this.runner != null) {
                        this.runner.interrupt();
                    }
                }
            }
        }

        public boolean isCompleted() {
            return this.len == this.partial.getEnd() - this.partial.getStart() - initOffset + 1;
        }
    }

    private static boolean isUri(@Nullable String filePath) {
        if (filePath == null) return false;
        return filePath.startsWith(ContentResolver.SCHEME_CONTENT) || filePath.startsWith(ContentResolver.SCHEME_FILE);
    }

}
